package webdav

import (
	"context"
	"io"
	"log"
	"net/http"
	"os"
	"path/filepath"
	"strings"
	"time"
)

type FileSystem2 interface {
	FileSystem
	ReadFile(ctx context.Context, w http.ResponseWriter, req *http.Request, original, name string) (File, error)
	WriteFile(ctx context.Context, w http.ResponseWriter, req *http.Request, original, name string, r io.ReadCloser, perm os.FileMode) error
	OpenFileLock(ctx context.Context, name string, flag int, perm os.FileMode) (File, error)
}

type Handler2 struct {
	Handler
	fileSystem FileSystem2
}

func NewHandler2(handler Handler, fileSystem FileSystem2) *Handler2 {
	handler.FileSystem = fileSystem
	return &Handler2{
		Handler:    handler,
		fileSystem: fileSystem,
	}
}

func (h *Handler2) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	status, err := http.StatusBadRequest, errUnsupportedMethod
	if h.FileSystem == nil {
		status, err = http.StatusInternalServerError, errNoFileSystem
	} else if h.LockSystem == nil {
		status, err = http.StatusInternalServerError, errNoLockSystem
	} else {
		switch r.Method {
		case "OPTIONS":
			status, err = h.handleOptions(w, r)
		case "GET", "HEAD", "POST":
			status, err = h.handleGetHeadPost(w, r)
		case "DELETE":
			status, err = h.handleDelete(w, r)
		case "PUT":
			status, err = h.handlePut(w, r)
		case "MKCOL":
			status, err = h.handleMkcol(w, r)
		case "COPY", "MOVE":
			status, err = h.handleCopyMove(w, r)
		case "LOCK":
			status, err = h.handleLock(w, r)
		case "UNLOCK":
			status, err = h.handleUnlock(w, r)
		case "PROPFIND":
			status, err = h.handlePropfind(w, r)
		case "PROPPATCH":
			status, err = h.handleProppatch(w, r)
		}
	}

	if status != 0 {
		w.WriteHeader(status)
		if status != http.StatusNoContent {
			_, _ = w.Write([]byte(StatusText(status)))
		}
	}
	if h.Logger != nil {
		h.Logger(r, err)
	}
}

func (h *Handler2) handleGetHeadPost(w http.ResponseWriter, r *http.Request) (status int, err error) {
	reqPath, status, err := h.stripPrefix(r.URL.Path)
	if err != nil {
		return status, err
	}

	// TODO: check locks for read-only access??
	ctx := r.Context()
	f, err := h.fileSystem.ReadFile(ctx, w, r, r.URL.Path, reqPath)
	if err != nil {
		return http.StatusNotFound, err
	}
	defer f.Close()
	fi, err := f.Stat()
	if err != nil {
		return http.StatusNotFound, err
	}
	if fi.IsDir() {
		return http.StatusMethodNotAllowed, nil
	}
	etag, err := findETag(ctx, h.FileSystem, h.LockSystem, reqPath, fi)
	if err != nil {
		return http.StatusInternalServerError, err
	}
	w.Header().Set("ETag", etag)

	// Let ServeContent determine the Content-Type header.
	http.ServeContent(w, r, reqPath, fi.ModTime(), f)
	return 0, nil
}

func (h *Handler2) handlePut(w http.ResponseWriter, r *http.Request) (status int, err error) {
	reqPath, status, err := h.stripPrefix(r.URL.Path)
	if err != nil {
		return status, err
	}
	release, status, err := h.confirmLocks(r, reqPath, "")
	if err != nil {
		return status, err
	}
	defer func() {
		if e := recover(); e != nil {
			log.Printf("panic in handler: %v, path: %v", e, r.URL.Path)
		}
		release()
	}()
	// TODO(rost): Support the If-Match, If-None-Match headers? See bradfitz'
	// comments in http.checkEtag.
	ctx := r.Context()

	err = h.fileSystem.WriteFile(ctx, w, r, r.URL.Path, reqPath, r.Body, 0666)
	if err != nil {
		return http.StatusMethodNotAllowed, err
	}
	fi, statErr := h.fileSystem.Stat(ctx, reqPath)
	// TODO(rost): Returning 405 Method Not Allowed might not be appropriate.
	if statErr != nil {
		return http.StatusMethodNotAllowed, statErr
	}
	etag, err := findETag(ctx, h.FileSystem, h.LockSystem, reqPath, fi)
	if err != nil {
		return http.StatusInternalServerError, err
	}
	w.Header().Set("ETag", etag)
	return http.StatusCreated, nil
}

func (h *Handler2) handleLock(w http.ResponseWriter, r *http.Request) (retStatus int, retErr error) {
	duration, err := parseTimeout(r.Header.Get("Timeout"))
	if err != nil {
		return http.StatusBadRequest, err
	}
	li, status, err := readLockInfo(r.Body)
	if err != nil {
		return status, err
	}

	ctx := r.Context()
	token, ld, now, created := "", LockDetails{}, time.Now(), false
	if li == (lockInfo{}) {
		// An empty lockInfo means to refresh the lock.
		ih, ok := parseIfHeader(r.Header.Get("If"))
		if !ok {
			return http.StatusBadRequest, errInvalidIfHeader
		}
		if len(ih.lists) == 1 && len(ih.lists[0].conditions) == 1 {
			token = ih.lists[0].conditions[0].Token
		}
		if token == "" {
			return http.StatusBadRequest, errInvalidLockToken
		}
		ld, err = h.LockSystem.Refresh(now, token, duration)
		if err != nil {
			if err == ErrNoSuchLock {
				return http.StatusPreconditionFailed, err
			}
			return http.StatusInternalServerError, err
		}

	} else {
		// Section 9.10.3 says that "If no Depth header is submitted on a LOCK request,
		// then the request MUST act as if a "Depth:infinity" had been submitted."
		depth := infiniteDepth
		if hdr := r.Header.Get("Depth"); hdr != "" {
			depth = parseDepth(hdr)
			if depth != 0 && depth != infiniteDepth {
				// Section 9.10.3 says that "Values other than 0 or infinity must not be
				// used with the Depth header on a LOCK method".
				return http.StatusBadRequest, errInvalidDepth
			}
		}
		reqPath, status, err := h.stripPrefix(r.URL.Path)
		if err != nil {
			return status, err
		}
		ld = LockDetails{
			Root:      reqPath,
			Duration:  duration,
			OwnerXML:  li.Owner.InnerXML,
			ZeroDepth: depth == 0,
		}
		token, err = h.LockSystem.Create(now, ld)
		if err != nil {
			if err == ErrLocked {
				return StatusLocked, err
			}
			return http.StatusInternalServerError, err
		}
		defer func() {
			if retErr != nil {
				h.LockSystem.Unlock(now, token)
			}
		}()

		// Create the resource if it didn't previously exist.
		if _, err := h.FileSystem.Stat(ctx, reqPath); err != nil {
			f, err := h.fileSystem.OpenFileLock(ctx, reqPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)
			if err != nil {
				// TODO: detect missing intermediate dirs and return http.StatusConflict?
				return http.StatusInternalServerError, err
			}
			f.Close()
			created = true
		}

		// http://www.webdav.org/specs/rfc4918.html#HEADER_Lock-Token says that the
		// Lock-Token value is a Coded-URL. We add angle brackets.
		w.Header().Set("Lock-Token", "<"+token+">")
	}

	w.Header().Set("Content-Type", "application/xml; charset=utf-8")
	if created {
		// This is "w.WriteHeader(http.StatusCreated)" and not "return
		// http.StatusCreated, nil" because we write our own (XML) response to w
		// and Handler.ServeHTTP would otherwise write "Created".
		w.WriteHeader(http.StatusCreated)
	}
	_, _ = writeLockInfo(w, token, ld)
	return 0, nil
}

// -------------------------------------------------------------------------------------------------------

type DirFileSystem2 struct {
	getRealFile func(string) (string, error)
	readFile    func(ctx context.Context, w http.ResponseWriter, req *http.Request, original, name string) (File, error)
	uploadFile  func(ctx context.Context, w http.ResponseWriter, req *http.Request, original, name string, r io.ReadCloser) error
}

func NewDirFileSystem2(
	getRealFile func(string) (string, error),
	readFile func(ctx context.Context, w http.ResponseWriter, req *http.Request, original, name string) (File, error),
	uploadFile func(ctx context.Context, w http.ResponseWriter, req *http.Request, original, name string, r io.ReadCloser) error,
) FileSystem2 {
	return &DirFileSystem2{
		getRealFile: getRealFile,
		readFile:    readFile,
		uploadFile:  uploadFile,
	}
}

func (fs *DirFileSystem2) resolve(name string) string {
	// This implementation is based on Dir.Open's code in the standard net/http package.
	if filepath.Separator != '/' && strings.IndexRune(name, filepath.Separator) >= 0 ||
		strings.Contains(name, "\x00") {
		return ""
	}

	getRealFile := fs.getRealFile
	if getRealFile == nil {
		return ""
	}

	filename, err := getRealFile(strings.ReplaceAll(slashClean(name), "\\", "/"))
	if err != nil {
		return ""
	}
	return filename
}

func (fs *DirFileSystem2) Mkdir(ctx context.Context, name string, perm os.FileMode) error {
	return nil
}

func (fs *DirFileSystem2) OpenFile(ctx context.Context, name string, flag int, perm os.FileMode) (File, error) {
	if flag != os.O_RDONLY {
		return nil, errUnsupportedMethod
	}

	if name = fs.resolve(name); name == "" {
		return nil, os.ErrNotExist
	}
	f, err := os.OpenFile(name, flag, perm)
	if err != nil {
		return nil, err
	}
	return f, nil
}

func (fs *DirFileSystem2) RemoveAll(ctx context.Context, name string) error {
	return errUnsupportedMethod
}

func (fs *DirFileSystem2) Rename(ctx context.Context, oldName, newName string) error {
	return errUnsupportedMethod
}

func (fs *DirFileSystem2) Stat(ctx context.Context, name string) (os.FileInfo, error) {
	if name = fs.resolve(name); name == "" {
		return nil, os.ErrNotExist
	}
	return os.Stat(name)
}

func (fs *DirFileSystem2) ReadFile(ctx context.Context, w http.ResponseWriter, req *http.Request, original, name string) (File, error) {
	if name = fs.resolve(name); name == "" {
		return nil, os.ErrNotExist
	}

	if readFile := fs.readFile; readFile == nil {
		return nil, errUnsupportedMethod
	} else {
		return readFile(ctx, w, req, original, name)
	}
}

func (fs *DirFileSystem2) WriteFile(ctx context.Context, w http.ResponseWriter, req *http.Request, original, name string, r io.ReadCloser, perm os.FileMode) error {
	if name = fs.resolve(name); name == "" {
		return os.ErrNotExist
	}

	if uploadFile := fs.uploadFile; uploadFile == nil {
		return errUnsupportedMethod
	} else {
		return uploadFile(ctx, w, req, original, name, r)
	}
}

func (fs *DirFileSystem2) OpenFileLock(ctx context.Context, name string, flag int, perm os.FileMode) (File, error) {
	if name = fs.resolve(name); name == "" {
		return nil, os.ErrNotExist
	}
	f, err := os.OpenFile(name, flag, perm)
	if err != nil {
		return nil, err
	}
	return f, nil
}
